cooperativeFilteringSvd = function(data, features_number, learning_rate, regularization_coeff, rating_range, epoch_nr = 120)
{
	init_value = 0.1
	init_value_q = init_value * init_value
	
	# get id of first user in train data set
	first_user = min(data[, 1])
	
	# get id of last user in train data set
	last_user = max(data[, 1])
	
	# get id of first movie in train data set
	first_movie = min(data[, 2])
	
	# get id of last movie in train data set
	last_movie = max(data[, 2])
	
	# count maximum users and movies number in data set
	max_user_number = last_user - first_user + 1
	max_movie_number = last_movie - first_movie + 1
	
	# init users and movies feature vectors with default value
	U = array(init_value, dim=c(max_user_number,features_number))
	V = array(init_value, dim=c(max_movie_number,features_number))
	
	# add cache column for every rating
	data$cache = 0
	
	res = svd.trainAllFeatures(data, U, V, features_number, learning_rate, regularization_coeff, rating_range, epoch_nr, init_value_q)
	
	# remember parameters passed to function used to build model
	res$features_number = features_number
	res$learning_rate = learning_rate
	res$regularization_coeff = regularization_coeff
	res$users_number = max_user_number
	res$movies_number = max_movie_number
	res$min_rating = min(data[,3])
	res$max_rating = max(data[,3])
	res$ratings_number = nrow(data)
	res$rating_range = rating_range
	
	res$call = match.call()
	class(res) = "svdCFModel"
	res	
	
}

svd.predictRating = function(user_feature_vector, movie_feature_vector, feature, features_number, cache, count_trailing_features, rating_range, init_value_q){
	
	if(cache > 0)
		sum = cache
	else
		sum = 0
	
	sum = sum + user_feature_vector[feature] * movie_feature_vector[feature]
	
	if(sum > rating_range[2]) sum = rating_range[2]
	else if(sum < rating_range[1]) sum = rating_range[1]	
	
	if(count_trailing_features){
		sum = sum + (features_number - feature) * init_value_q
		if(sum > rating_range[2]) sum = rating_range[2]
		else if(sum < rating_range[1]) sum = rating_range[1]
	}
		
	return (sum)
}


svd.trainAllFeatures = function(train_data, U, V, features_number, learning_rate, regularization_coeff, rating_range, epoch_nr, init_value_q){
	
	start_time = Sys.time()
	
	train_data = as.matrix(train_data)
	
	# loop through all features
	for(feature in 1:features_number){
		
		# reset current RMSE for training of every new feature
		current_RMSE = 1.0
		flag = TRUE
		loop_number = 0
		
		repeat{
			
			loop_number = loop_number + 1
			feature_sum_error = 0
			previous_RMSE = current_RMSE
			
			# go trough all ratings and update feature values
			for (i in 1:nrow(train_data)) {	
				
				# compute difference between true rating and predicted
				diff = train_data[i, 3] - svd.predictRating(U[train_data[i, 1],], V[train_data[i, 2],], feature, features_number, train_data[i, "cache"], TRUE, rating_range, init_value_q)
				feature_sum_error = feature_sum_error + diff * diff
				
				# compute gradients
				G1 = diff * V[train_data[i, 2], feature] - regularization_coeff * U[train_data[i, 1], feature]
				G2 = diff * U[train_data[i, 1], feature] - regularization_coeff * V[train_data[i, 2], feature]
				
				# compute new values of currently trained feature
				U[train_data[i, 1], feature] = U[train_data[i, 1], feature] + learning_rate * G1
				V[train_data[i, 2], feature] = V[train_data[i, 2], feature] + learning_rate * G2 
				
			}
			
			# calculate new RMSE
			current_RMSE = sqrt(feature_sum_error / nrow(train_data))			
			
			# if error starts to rise break training feature
			if(flag){
				flag = FALSE
			}else if(current_RMSE > previous_RMSE){
				break
			}else if(loop_number >= epoch_nr){
				break
			}
			
		} # repeat end
		
		# cache errors calculated from trained feature
		for (i in 1:nrow(train_data)){
			train_data[i, "cache"] = svd.predictRating(U[train_data[i, 1],], V[train_data[i, 2],], feature, features_number, train_data[i, "cache"], FALSE, rating_range, init_value_q)
		}
		
		cat(sprintf("End of training feature: %d\n", feature))				
	} # for end	
		
	cat(paste("Model built in", sprintf("%.2f", difftime(Sys.time(), start_time, units="mins")), "minutes.\n"))
	return (list(U = U, V = V))
	
}

print.svdCFModel = function(x, ...){
	
	cat("Call:\n")
	print(x$call)
	
	cat("\nFeatures number:\n")
	print(x$features_number)
	
	cat("\nLearning rate:\n")
	print(x$learning_rate)
	
	cat("\nRegularization coefficient:\n")
	print(x$regularization_coeff)
	
	cat("\nUsers number:\n")
	print(x$users_number)		
	
	cat("\nMovies number:\n")
	print(x$movies_number)		
	
}

summary.svdCFModel = function(x, ...){
	
	cat("Call:\n")
	print(x$call)	
	
	cat(paste("\nModel built on", x$ratings_number, "ratings.\n"))
	
	cat("\nMinimal rating:\n")
	print(x$min_rating)
	
	cat("\nMaximal rating:\n")
	print(x$max_rating)
	
}

predict.svdCFModel = function(x, user_id, movie_id){
	
	rating = 0
	for(fn in 1:x$features_number){
		rating = rating + x$U[user_id, fn] * x$V[movie_id, fn]
		if(rating > x$rating_range[2]) rating = 5
		else if(rating < x$rating_range[1]) rating = 1
	}
	
	round(rating)	
}

